package com.vaadin.data;

import com.vaadin.data.util.MethodProperty;
import com.vaadin.data.util.filter.SimpleStringFilter;
import com.vaadin.data.util.filter.UnsupportedFilterException;
import org.apache.cayenne.*;
import org.apache.cayenne.exp.Expression;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.map.ObjAttribute;
import org.apache.cayenne.query.Ordering;
import org.apache.cayenne.query.QueryCacheStrategy;
import org.apache.cayenne.query.SelectQuery;
import org.apache.cayenne.query.SortOrder;
import org.apache.cayenne.reflect.*;

import java.io.Serializable;
import java.lang.ref.WeakReference;
import java.lang.reflect.Constructor;
import java.util.*;

/**
 * Created by IntelliJ IDEA.
 * User: Dmitry Zhuravlev
 * Date: 26/09/11
 * Time: 11:16
 * Cayenne container based on HbnContainer
 */
public class CayenneContainer<T extends Persistent> implements Container.Indexed, Container.Sortable,
                                                               Container.ItemSetChangeNotifier, Container.Filterable {

  private static final int REFERENCE_CLEANUP_INTERVAL = 2000;
  private static final long serialVersionUID = -6410337120924382057L;

  /**
   * ContextManager interface is used by CayenneContainer to get reference to
   * Cayennes Session object.
   */
  public interface ContextManager {
    /**
     * @return a Cayenne Session with open transaction
     */
    ObjectContext getCayenneContext();
  }

  public void refresh() {
    clearInternalCache();
    fireItemSetChange();
  }

  /**
   * Item wrappping a Cayenne mapped entity object. EntityItems are
   * generally instantiated automatically by CayenneContainer.
   */
  @SuppressWarnings("hiding")
  public class EntityItem<T extends Persistent> implements Item {

    private static final long serialVersionUID = -2847179724504965599L;

    /**
     * Reference to cayenne mapped entity that this Item wraps.
     */
    protected T pojo;

    /**
     * Instantiated properties of this EntityItem. May be either
     * EntityItemProperty (cayenne field) or manually added container
     * property (MethodProperty).
     */
    protected Map<Object, Property> properties = new HashMap<Object, Property>();

    @SuppressWarnings("unchecked")
    public EntityItem(ObjectId id) {

      pojo = (T)DataObjectUtils.objectForPK(contextManager.getCayenneContext(), id);

      // add non-cayenne mapped container properties
      for (String propertyId : addedProperties.keySet()) {
        addItemProperty(propertyId,
                        new MethodProperty(pojo, propertyId));
      }
    }

    /**
     * @return the wrapped entity object.
     */
    public T getPojo() {
      return pojo;
    }

    public boolean addItemProperty(Object id, Property property)
      throws UnsupportedOperationException {
      properties.put(id, property);
      return true;
    }

    public Property getItemProperty(Object id) {
      Property p = properties.get(id);
      if (p == null) {
        p = new EntityItemProperty(id.toString());
        properties.put(id, p);
      }
      return p;
    }

    public Collection<?> getItemPropertyIds() {
      return getContainerPropertyIds();
    }

    public boolean removeItemProperty(Object id)
      throws UnsupportedOperationException {
      Property removed = properties.remove(id);
      return removed != null;
    }

    /**
     * EntityItemProperty wraps one Cayenne controlled field of the pojo
     * used by EntityItem. For common fields the field value is the same as
     * Property value. For relation fields it is the identifier of related
     * object or a collection of identifiers.
     */
    public class EntityItemProperty implements Property,
                                               Property.ValueChangeNotifier {

      private static final long serialVersionUID = -4086774943938055297L;
      private final String propertyName;

      public EntityItemProperty(String propertyName) {
        this.propertyName = propertyName;
      }

      public EntityItem<T> getEntityItem() {
        return EntityItem.this;
      }

      public T getPojo() {
        return pojo;
      }

      /**
       * A helper method to get raw type of this (Cayenne) property.
       *
       * @return the raw type of field
       */
      private org.apache.cayenne.reflect.Property getProperty() {
        return getClassDescriptor().getProperty(propertyName);
      }

      private boolean isAssociationProperty(org.apache.cayenne.reflect.Property property) {
        return (property instanceof ToManyProperty || property instanceof ToOneProperty);
      }

      public Class<?> getType() {
        if (getProperty() instanceof AttributeProperty) {
          ObjAttribute attribute = ((AttributeProperty)getProperty()).getAttribute();
          return attribute.getJavaClass();
        }
        if (getProperty() instanceof ToOneProperty) {
          return ((ToOneProperty)getProperty()).getTargetDescriptor().getObjectClass();
        }
        if (getProperty() instanceof ToManyProperty) {
          return ((ToManyProperty)getProperty()).getTargetDescriptor().getObjectClass();
        }
        else {
          throw new UnsupportedOperationException("Get type of non Attribute propery unsupported yet");
        }
      }

      @SuppressWarnings("unchecked")
      public Object getValue() {
        pojo = (T)contextManager.getCayenneContext().localObject((pojo).getObjectId(), pojo);
        org.apache.cayenne.reflect.Property property = getProperty();
        if (!isAssociationProperty(property)) {
          return getClassDescriptor().getProperty(propertyName).readProperty(pojo);
        }
        else {
          /*
          * the return value will be the identifier of referenced
          * object
          */
          //TODO think about how to LAZY load ToMany relationship in cayenne!!
          if (property instanceof ToManyProperty) {
            Collection<Persistent> pojos = (Collection<Persistent>)((DataObject)pojo).readProperty(propertyName);
            List<ObjectId> identifiers = new ArrayList<ObjectId>(pojos.size());
            for (Persistent p : pojos) {
              identifiers.add(p.getObjectId());
            }
            return identifiers;
          }
          else if (property instanceof ToOneProperty) {
            return ((Persistent)((DataObject)pojo).readProperty(propertyName)).getObjectId();
          }
          else if (property instanceof AttributeProperty) {
            return ((DataObject)pojo).readProperty(propertyName);
          }
          else {
            throw new UnsupportedOperationException("Unknown property " + property.getName() + " " + property.getClass());
          }
        }
      }

      public boolean isReadOnly() {
        // TODO
        return false;
      }

      public void setReadOnly(boolean newStatus) {
        throw new UnsupportedOperationException();
      }

      public void setValue(Object newValue) throws ReadOnlyException,
                                                   ConversionException {
        org.apache.cayenne.reflect.Property property = getProperty();

        try {
          Object value;
          try {
            if (!isAssociationProperty(property)) {
              if (newValue == null
                  || getType().isAssignableFrom(
                newValue.getClass())) {
                value = newValue;
              }
              else {
                // Gets the string constructor
                final Constructor<?> constr = getType()
                  .getConstructor(
                    new Class[]{String.class});

                value = constr.newInstance(newValue.toString());
              }
              property.writeProperty(getPojo(), null, value);
            }
            else if (property instanceof ToOneProperty) {
              Object newPojoValue = getPojoByID((ObjectId)newValue);
              property.writeProperty(getPojo(), null, newPojoValue);
            }
            // Persist (possibly) detached pojo
            @SuppressWarnings("unchecked")
            T newPojo = (T)contextManager.getCayenneContext().localObject(getPojo().getObjectId(), null);
            pojo = newPojo;

            fireValueChange();
          }
          catch (final java.lang.Exception e) {
            e.printStackTrace();
            throw new Property.ConversionException(e);
          }
        }
        catch (CayenneRuntimeException e) {
          e.printStackTrace();
        }
      }

      @Override
      public String toString() {
        Object v = getValue();
        if (v != null) {
          return v.toString();
        }
        else {
          return null;
        }
      }

      private class CayennePropertyValueChangeEvent implements
                                                    Property.ValueChangeEvent {
        private static final long serialVersionUID = 166764621324404579L;

        public Property getProperty() {
          return EntityItemProperty.this;
        }
      }

      private List<ValueChangeListener> valueChangeListeners;

      private void fireValueChange() {
        if (valueChangeListeners != null) {
          CayennePropertyValueChangeEvent event = new CayennePropertyValueChangeEvent();
          Object[] array = valueChangeListeners.toArray();
          for (Object anArray : array) {
            ((ValueChangeListener)anArray).valueChange(event);
          }
        }
      }

      public void addListener(ValueChangeListener listener) {
        if (valueChangeListeners == null) {
          valueChangeListeners = new LinkedList<ValueChangeListener>();
        }
        if (!valueChangeListeners.contains(listener)) {
          valueChangeListeners.add(listener);
        }
      }

      public void removeListener(ValueChangeListener listener) {
        if (valueChangeListeners != null) {
          valueChangeListeners.remove(listener);
        }
      }
    }
  }

  private static final int ROW_BUF_SIZE = 100;
  private static final int ID_TO_INDEX_MAX_SIZE = 300;

  /**
   * Entity class that will be listed in container
   */
  protected Class<T> type;
  protected final ContextManager contextManager;
  private ClassDescriptor classDescriptor;

  private String orderPropertyName;

  /**
   * internal flag used to temporarily invert order of listing
   */
  private boolean normalOrder = true;

  /**
   * Row buffer of pojos, used to optimize query count when iterating forward
   */
  private List<T> ascRowBuffer;
  /**
   * Row buffer of pojos, used to optimize query count when iterating backward
   */

  private List<T> descRowBuffer;
  /**
   * cached last item identifier
   */
  private Object lastId;
  /**
   * cached first item identifier
   */
  private Object firstId;

  /**
   * Row buffer of pojos, used to optimize query count when container is
   * accessed with indexes
   */
  private List<T> indexRowBuffer;

  /**
   * Container wide index of the first entity in indexRowBuffer
   */
  private int indexRowBufferFirstIndex;

  /**
   * Map from entity/item identifiers to index. Maps does not contain mapping
   * for all identifiers in container, but only those that are recently
   * loaded. Map gets cleanded during usage, to free memory.
   */
  private final Map<Object, Integer> idToIndex = new LinkedHashMap<Object, Integer>();

  /**
   * whether sorts are made ascending or descending, see
   * {@link #orderPropertyIds}
   */
  private boolean[] orderAscendings;
  /**
   * Properties among container is sorted by. Used to implement
   * Container.Sortable
   */
  private Object[] orderPropertyIds;

  /**
   * Cached size of container. Used to optimize query count.
   */
  private Integer size;

  private LinkedList<ItemSetChangeListener> itemSetChangeListeners;

  /**
   * Contains current filters which has been applied to this container. Used
   * to implement Container.Filterable.
   */
  private HashSet<ContainerFilter> filters;

  /**
   * Caches weak references to items, in order to conserve memory.
   */
  private final HashMap<Object, WeakReference<EntityItem<T>>> itemCache = new HashMap<Object, WeakReference<EntityItem<T>>>();

  /**
   * A map of added javabean property names to their respective types
   */
  private final Map<String, Class<?>> addedProperties = new HashMap<String, Class<?>>();

  /**
   * counter for items loaded by container, used to implement cleanup of
   * weakreferences
   */
  private int loadCount;

  /**
   * Creates a new instance of CayenneContainer, listing all object of given type
   * from database.
   *
   * @param entityType Entity class to be listed in container.
   * @param contextMgr interface via Cayenne session is fetched
   */
  public CayenneContainer(Class<T> entityType, String orderPropertyName, ContextManager contextMgr) {
    type = entityType;
    contextManager = contextMgr;
    this.orderPropertyName = orderPropertyName;
  }

  /**
   * CayenneContainer automatically adds all fields that are mapped by Cayenne
   * to DB. With this method one can add a javabean property to the container
   * that is contained on pojo but not cayenne-mapped.
   *
   * @see Container#addContainerProperty(Object, Class, Object)
   */
  public boolean addContainerProperty(Object propertyId, Class<?> type,
                                      Object defaultValue) throws UnsupportedOperationException {
    boolean propertyExists = true;
    try {
      new MethodProperty(this.type.newInstance(), propertyId.toString());
    }
    catch (InstantiationException ex) {
      ex.printStackTrace();
      propertyExists = false;
    }
    catch (IllegalAccessException ex) {
      ex.printStackTrace();
      propertyExists = false;
    }
    addedProperties.put(propertyId.toString(), type);
    return propertyExists;
  }

  /**
   * CayenneContainer specific method to persist newly created entity.
   *
   * @param entity the unsaved entity object
   * @return the identifier for newly persisted entity
   */
  public ObjectId saveEntity(T entity) {
    // insert into DB
    if (entity.getPersistenceState() == PersistenceState.TRANSIENT) {
      contextManager.getCayenneContext().registerNewObject(entity);
    }
    contextManager.getCayenneContext().commitChanges();
    refresh();
    return getIdForPojo(entity);
  }

  /**
   * CayenneContainer specific method to update entity.
   *
   * @param entity to update
   * @return the identifier of the updated entity
   */
  public Serializable updateEntity(T entity) {
    // update DB
    if (entity.getPersistenceState() == PersistenceState.TRANSIENT) {
      contextManager.getCayenneContext().registerNewObject(entity);
    }
    contextManager.getCayenneContext().commitChanges();

    EntityItem<T> item = null;
    ObjectId itemId = getIdForPojo(entity);

    if (itemCache != null) {
      // refresh itemCache
      WeakReference<EntityItem<T>> weakReference = itemCache.get(itemId);
      if (weakReference != null) {
        item = weakReference.get();
        if (item != null) { // may be already collected, but not cleaned
          item.pojo = entity;
        }
      }
    }

    if (item != null) { // if it was in cache, it might be rendered
      // fire change events on this item, properties might have changed
      for (Object id : item.getItemPropertyIds()) {
        Property p = item.getItemProperty(id);
        if (p instanceof EntityItem.EntityItemProperty) {
          EntityItem.EntityItemProperty ep = (EntityItem.EntityItemProperty)p;
          ep.fireValueChange();
        }
      }
    }

    return itemId;
  }

  public Object addItem() throws UnsupportedOperationException {
    throw new UnsupportedOperationException("Cayenne objects should be created outside container!");
    /*Object o;
    try {

      // create a new instance of entity type
      o = type.newInstance();
      // insert into DB
      contextManager.getCayenneContext().registerNewObject(o);
      // we need to clear internal caches of CayenneContainer
      // notify listeners that a new item has been added, will cause eg.
      // Table refresh
      refresh();
      return getIdForPojo(o);
    }
    catch (InstantiationException e) {
      e.printStackTrace();
      return null;
    }
    catch (IllegalAccessException e) {
      e.printStackTrace();
      return null;
    }*/
  }


  @SuppressWarnings("unchecked")
  public Item addItem(Object itemId) throws UnsupportedOperationException {
    // Expecting autogenerated identifiers
    Item newItem = null;
    try {
      // persistent entity with given id exist in context
      T p = (T)DataObjectUtils.objectForPK(contextManager.getCayenneContext(), (ObjectId)itemId);
      if (p != null) {
        saveEntity(p);
        newItem = new EntityItem<T>((ObjectId)itemId);
      }
    }
    catch (CayenneRuntimeException e) {
      e.printStackTrace();
      return null;
    }
    return newItem;
  }

  public boolean containsId(Object itemId) {
    // test if entity can be found with given id
    try {

      return (DataObjectUtils.objectForPK(contextManager.getCayenneContext(), (ObjectId)itemId) != null);
    }
    catch (Exception e) {
      // this should not happen if used correctly
      e.printStackTrace();
      return false;
    }
  }

  public Property getContainerProperty(Object itemId, Object propertyId) {
    return getItem(itemId).getItemProperty(propertyId);
  }

  @SuppressWarnings("unchecked")
  public Collection getContainerPropertyIds() {
    Collection propertyIds = getSortableContainerPropertyIds();
    propertyIds.addAll(addedProperties.keySet());
    return propertyIds;
  }

  /**
   * Helper method to get the Cayenne ClassMetadata for listed entity type.
   * Method lazyly caches the metadata, optimizing the lookup a bit (instead
   * of fetching it continuously from Session).
   *
   * @return Cayennes ClassMetadata for the listed entity type
   */
  private ClassDescriptor getClassDescriptor() {

    return getClassDescriptor(type);
  }

  private ClassDescriptor getClassDescriptor(Class<?> clazz) {
    if (classDescriptor == null) {
      classDescriptor = contextManager.getCayenneContext().getEntityResolver().getClassDescriptor(clazz.getSimpleName());
    }
    return classDescriptor;
  }

  public EntityItem<T> getItem(Object itemId) {
    EntityItem<T> item = null;
    if (itemId != null) {
      item = loadItem((ObjectId)itemId);
    }
    return item;
  }

  /**
   * This method is used to fetch Items by id. Override this if you need
   * customized EntityItems.
   *
   * @param itemId
   * @return
   */
  protected EntityItem<T> loadItem(ObjectId itemId) {
    // clean up refQue if there are some items to clean up
    cleanCache();

    EntityItem<T> item;
    // Search the itemCache if the entityitem is already loaded
    WeakReference<EntityItem<T>> weakReference = itemCache.get(itemId);
    if (weakReference != null) {
      item = weakReference.get();
      // still check if weakreference still contained the item (may be
      // carbagecollected, but not cleand from cache map
      if (item != null) {
        // return the previously instantiated entityitem
        return item;
      }
    }

    item = new EntityItem<T>(itemId);
    itemCache.put(itemId, new WeakReference<EntityItem<T>>(item));
    return item;
  }

  /**
   * Cleans the itemCache of collected item references. This method run every
   * now and then by {@link #loadItem(ObjectId)}, but may be run manually
   * too.
   * <p/>
   * <p/>
   * TODO figure out if this is the best possible way to free the memory
   * consumed by (empty) weak references and open this mechanism for extension
   */
  private void cleanCache() {
    if (++loadCount % REFERENCE_CLEANUP_INTERVAL == 0) {
      Set<Map.Entry<Object, WeakReference<EntityItem<T>>>> entries = itemCache
        .entrySet();
      for (Iterator<Map.Entry<Object, WeakReference<EntityItem<T>>>> iterator = entries
        .iterator(); iterator.hasNext(); ) {
        Map.Entry<Object, WeakReference<EntityItem<T>>> entry = iterator
          .next();
        if (entry.getValue().get() == null) {
          // if the referenced entityitem is carbage collected, remove
          // the weak reference itself
          iterator.remove();
        }
      }
    }
  }

  public Collection<?> getItemIds() {
    /*
    * Create an optimized query to return only identifiers. Note that this
    * method does not scale well for large database. At least Table is
    * optimized so that it does not call this method.
    */
    SelectQuery crit = getBaseQuery();
    //TODO need rewrite! return only identifiers
    return contextManager.getCayenneContext().performQuery(crit);
  }

  public Class<?> getType(Object propertyId) {
    /*
    * This method does pretty much the same thing as
    * EntityItemProperty#getType()
    *
    */
    if (addedProperties.keySet().contains(propertyId)) {
      return addedProperties.get(propertyId);
    }
    org.apache.cayenne.reflect.Property propertyType = getClassDescriptor().getProperty(
      propertyId.toString());
    return propertyType.getClass();
  }


  public boolean removeAllItems() throws UnsupportedOperationException {
    // TODO
    return false;
  }

  /* Remove a container property added with addContainerProperty() */
  public boolean removeContainerProperty(Object propertyId)
    throws UnsupportedOperationException {
    boolean propertyExisted = false;
    Class<?> removed = addedProperties.remove(propertyId);
    if (removed != null) {
      propertyExisted = true;
    }
    return propertyExisted;
  }

  public boolean removeItem(Object itemId)
    throws UnsupportedOperationException {
    Object p = DataObjectUtils.objectForPK(contextManager.getCayenneContext(), (ObjectId)itemId);
    // remove row from db
    contextManager.getCayenneContext().deleteObject(p);
    contextManager.getCayenneContext().commitChanges();
    // clear internal caches and notify listeners that item has been removed
    refresh();
    return true;
  }

  public void addListener(ItemSetChangeListener listener) {
    if (itemSetChangeListeners == null) {
      itemSetChangeListeners = new LinkedList<ItemSetChangeListener>();
    }
    itemSetChangeListeners.add(listener);
  }

  public void removeListener(ItemSetChangeListener listener) {
    if (itemSetChangeListeners != null) {
      itemSetChangeListeners.remove(listener);
    }
  }

  private void fireItemSetChange() {
    if (itemSetChangeListeners != null) {
      final Object[] l = itemSetChangeListeners.toArray();
      final Container.ItemSetChangeEvent event = new Container.ItemSetChangeEvent() {
        private static final long serialVersionUID = -3002746333251784195L;

        public Container getContainer() {
          return CayenneContainer.this;
        }
      };
      for (Object aL : l) {
        ((ItemSetChangeListener)aL).containerItemSetChange(event);
      }
    }
  }

  public int size() {
    if (size == null) {
      /*
      * If cached size does not exist, query from database
      */
      SelectQuery query = new SelectQuery(type);
      query.setPageSize(1);
      size = contextManager.getCayenneContext().performQuery(query).size();
    }
    return size;
  }

  public Object addItemAfter(Object previousItemId)
    throws UnsupportedOperationException {
    // can't implement properly for database backed container like this
    throw new UnsupportedOperationException();
  }

  public Item addItemAfter(Object previousItemId, Object newItemId)
    throws UnsupportedOperationException {
    // can't implement properly for database backed container like this
    throw new UnsupportedOperationException();
  }

  /**
   * Gets a base listing using current orders etc.
   *
   * @return criteria with current Order criterias added
   */
  private SelectQuery getBaseQuery() {
    Expression expr = getBaseExpression();
    List<Ordering> orders = getOrder(orderPropertyName, !normalOrder);
    SelectQuery query;
    if (expr == null) {
      query = new SelectQuery(type);
    }
    else {
      query = new SelectQuery(type, expr);
    }
    for (Ordering order : orders) {
      query.addOrdering(order);
    }
    return query;
  }

  /**
   * Return the ordering criteria in the order in which they should be
   * applied. The composed order must be stable and must include
   * {@link #getNaturalOrder(String, boolean)} at the end.
   *
   * @param flipOrder reverse the order if true
   * @return List<Order> orders to apply, first item has the highest priority
   */
  protected final List<Ordering> getOrder(String orderPropertyName, boolean flipOrder) {
    List<Ordering> orders = new ArrayList<Ordering>();

    // standard sort order set by the user by property
    orders.addAll(getDefaultOrder(flipOrder));

    // natural order
    orders.add(getNaturalOrder(orderPropertyName, flipOrder));

    return orders;
  }

  /**
   * Returns the ordering to use for the container contents. The default
   * implementation provides the {@link Container.Sortable} functionality.
   * <p/>
   * Can be overridden to customize item sort order.
   *
   * @param flipOrder reverse the order if true
   * @return List<Order> orders to apply, first item has the highest priority
   */
  protected List<Ordering> getDefaultOrder(boolean flipOrder) {
    List<Ordering> orders = new ArrayList<Ordering>();
    if (orderPropertyIds != null) {
      for (int i = 0; i < orderPropertyIds.length; i++) {
        String orderPropertyId = orderPropertyIds[i].toString();

        boolean a = flipOrder ? !orderAscendings[i]
                              : orderAscendings[i];
        if (a) {
          orders.add(new Ordering(orderPropertyId, SortOrder.ASCENDING));
        }
        else {

          orders.add(new Ordering(orderPropertyId, SortOrder.DESCENDING));
        }
      }
    }
    return orders;
  }

  /**
   * This method is meant to be called by CayenneContainer itself. It will create
   * the base criteria for entity class and add possible restrictions to
   * query. This method is protected so developers can add their own custom
   * criterias.
   *
   * @return
   */
  protected Expression getBaseExpression() {
    Expression expr = null;
    if (filters != null) {
      for (ContainerFilter filter : filters) {
        // convert ContainerFilters to cayenne Restriction Criterias
        String filterPropertyName = filter.propertyId.toString();
        if (filter.ignoreCase) {
          expr = ExpressionFactory.likeIgnoreCaseExp(filterPropertyName, filter.filterString);
        }
        else {
          expr = ExpressionFactory.likeExp(filterPropertyName, filter.filterString);
        }
      }
    }
    return expr;
  }

  /**
   * Natural order is the order in which the database is sorted if container
   * has no other ordering set. Natural order is always added as least
   * significant order to queries. This is needed to keep items stable order
   * across queries.
   * <p/>
   * The default implementation sorts entities by identifier column.
   *
   * @param flipOrder
   * @return
   */
  protected Ordering getNaturalOrder(String orderPropertyName, boolean flipOrder) {
    if (flipOrder) {
      return new Ordering(orderPropertyName, SortOrder.DESCENDING);
    }
    else {
      return new Ordering(orderPropertyName, SortOrder.ASCENDING);
    }
  }

  public Object firstItemId() {
    if (firstId == null) {
      // firstId was not cached, query the first item from db
      firstId = firstItemId(true);
    }
    return firstId;
  }

  /**
   * Internanal helper method to implement {@link #firstItemId()} and
   * {@link #lastItemId()}.
   */
  protected Object firstItemId(boolean byPassCache) {
    if (byPassCache) {
      SelectQuery query = getBaseQuery();
      query.setFetchLimit(1);
      query.setCacheStrategy(QueryCacheStrategy.LOCAL_CACHE);
      T first = (T)contextManager.getCayenneContext().performQuery(query).get(0);
      Object id = getIdForPojo(first);
      idToIndex.put(id, normalOrder ? 0 : size() - 1);
      return id;
    }
    else {
      return firstItemId();
    }
  }

  /**
   * Helper method to detect identifier of given entity object.
   *
   * @param pojo the entity object which identifier is to be resolved
   * @return the identifier if the given Cayenne entity object
   */
  private ObjectId getIdForPojo(Object pojo) {
    return ((Persistent)pojo).getObjectId();
  }

  private Object getPojoByID(ObjectId id) {
    return DataObjectUtils.objectForPK(contextManager.getCayenneContext(), id);
  }

  public boolean isFirstId(Object itemId) {
    return itemId.equals(firstItemId());
  }

  public boolean isLastId(Object itemId) {
    return itemId.equals(lastItemId());
  }

  public Object lastItemId() {
    if (lastId == null) {
      normalOrder = !normalOrder;
      lastId = firstItemId(true);
      normalOrder = !normalOrder;
    }
    return lastId;
  }

  /*
  * Simple method, but lot's of code :-)
  *
  * Rather complicated logic is needed to avoid:
  *
  * 1. large number of db queries
  *
  * 2. scrolling through whole query result
  *
  * This way this container can be used with large data sets.
  */
  public Object nextItemId(Object itemId) {
    // TODO should not call if know that next exists based on cache, would
    // optimize one query in some situations
    if (isLastId(itemId)) {
      return null;
    }

    EntityItem<T> item = new EntityItem<T>((ObjectId)itemId);

    // check if next itemId is in current buffer
    List<T> buffer = getRowBuffer();
    try {
      int curBufIndex = buffer.indexOf(item.getPojo());
      if (curBufIndex != -1) {
        T object = buffer.get(curBufIndex + 1);
        return getIdForPojo(object);
      }
    }
    catch (Exception e) {
      // not in buffer
    }

    // itemId was not in buffer
    // build query with current order and limiting result set with the
    // reference row. Then first result is next item.

    int currentIndex = indexOfId(itemId);
    int firstIndex = normalOrder ? currentIndex + 1 : size() - currentIndex
                                                      - 1;

    SelectQuery query = getBaseQuery();
    query.setPageSize(ROW_BUF_SIZE);
    query.setFetchOffset(firstIndex);
    @SuppressWarnings("unchecked")
    List<T> res = contextManager.getCayenneContext().performQuery(query);
    List<T> newBuffer;
    if (res.size() < ROW_BUF_SIZE) {
      newBuffer = res;
    }
    else {
      newBuffer = res.subList(firstIndex, ROW_BUF_SIZE);
    }

    if (newBuffer.size() > 0) {
      // save buffer to optimize query count
      setRowBuffer(newBuffer, firstIndex);
      T nextPojo = newBuffer.get(0);
      return getIdForPojo(nextPojo);
    }
    else {
      return null;
    }
  }

  /**
   * RowBuffer stores a list of entity items to avoid excessive number of DB
   * queries.
   *
   * @return
   */
  private List<T> getRowBuffer() {
    if (normalOrder) {
      return ascRowBuffer;
    }
    else {
      return descRowBuffer;
    }
  }

  /**
   * RowBuffer stores some pojos to avoid excessive number of DB queries.
   * <p/>
   * Also updates the idToIndex map.
   */
  private void setRowBuffer(List<T> list, int firstIndex) {
    if (normalOrder) {
      ascRowBuffer = list;
      for (int i = 0; i < list.size(); ++i) {
        idToIndex.put(getIdForPojo(list.get(i)), firstIndex + i);
      }
    }
    else {
      descRowBuffer = list;
      int lastIndex = size() - 1;
      for (int i = 0; i < list.size(); ++i) {
        idToIndex.put(getIdForPojo(list.get(i)), lastIndex - firstIndex
                                                 - i);
      }
    }
  }

  public Object prevItemId(Object itemId) {
    // temp flip order and use nextItemId implementation
    normalOrder = !normalOrder;
    Object prev = nextItemId(itemId);
    normalOrder = !normalOrder;
    return prev;
  }


  /**
   * Not supported in CayenneContainer. Indexing/order is controlled by
   * underlaying database.
   */
  public Object addItemAt(int index) throws UnsupportedOperationException {
    throw new UnsupportedOperationException();
  }

  /**
   * Not supported in CayenneContainer. Indexing/order is controlled by
   * underlaying database.
   */
  public Item addItemAt(int index, Object newItemId)
    throws UnsupportedOperationException {
    throw new UnsupportedOperationException();
  }

  public Object getIdByIndex(int index) {
    if (indexRowBuffer == null) {
      resetIndexRowBuffer(index);
    }
    int indexInCache = index - indexRowBufferFirstIndex;
    if (!(indexInCache >= 0 && indexInCache < indexRowBuffer.size())) {
      /*
      * If requested index is not currently in cache, reset it starting
      * from queried index.
      */
      resetIndexRowBuffer(index);
      indexInCache = 0;
    }
    T pojo = indexRowBuffer.get(indexInCache);
    Object id = getIdForPojo(pojo);
    idToIndex.put(id, new Integer(index));
    if (idToIndex.size() > ID_TO_INDEX_MAX_SIZE) {
      // clear one from beginning, if ID_TO_INDEX_MAX_SIZE is total of all
      // caches, only detached indexes should get removed
      idToIndex.remove(idToIndex.keySet().iterator().next());
    }
    return id;
  }

  /**
   * Helper method to query new set of entity items to cache from given index.
   *
   * @param index the index of first entity object ot be included in query
   */
  @SuppressWarnings("unchecked")
  private void resetIndexRowBuffer(int index) {
    indexRowBufferFirstIndex = index;
    SelectQuery query = getBaseQuery();
    query.setFetchOffset(index);
    query.setPageSize(50);
    query.setFetchLimit(ROW_BUF_SIZE);
    indexRowBuffer = contextManager.getCayenneContext().performQuery(query);
  }

  /*
  * Note! Expects that getIdByIndex is called for this itemId. Otherwise it
  * will be potentially rather slow operation with large tables. When used
  * with Table, this shouldn't be a problem.
  *
  */
  public int indexOfId(Object itemId) {
    Integer index = idToIndex.get(itemId);
    if (index == null) {
      return slowIndexOfId(itemId);
    }
    return index;
  }

  private int slowIndexOfId(Object itemId) {

    SelectQuery query = getBaseQuery();
    query.setPageSize(1);
    List<?> list = contextManager.getCayenneContext().performQuery(query);
    return list.indexOf(itemId);
  }

  // Container.Sortable methods

  public Collection<String> getSortableContainerPropertyIds() {
    // use Cayennes metadata helper to determine property names
    final ArrayList<String> propertyNames = new ArrayList<String>();
    getClassDescriptor().visitAllProperties(new PropertyVisitor() {
      @Override
      public boolean visitAttribute(AttributeProperty property) {
        propertyNames.add(property.getName());
        return true;
      }

      @Override
      public boolean visitToOne(ToOneProperty property) {
        propertyNames.add(property.getName());
        return true;
      }

      @Override
      public boolean visitToMany(ToManyProperty property) {
        propertyNames.add(property.getName());
        return true;
      }
    });

    LinkedList<String> propertyIds = new LinkedList<String>();
    propertyIds.addAll(propertyNames);
    return propertyIds;
  }

  public void sort(Object[] propertyId, boolean[] ascending) {
    // we do not actually sort anything here, just clearing cache will do
    // the thing lazily.
    clearInternalCache();
    orderPropertyIds = propertyId;
    orderAscendings = ascending;
  }

  /**
   * Helper method to clear all cache fields.
   */
  protected void clearInternalCache() {
    idToIndex.clear();
    indexRowBuffer = null;
    ascRowBuffer = null;
    descRowBuffer = null;
    firstId = null;
    lastId = null;
    size = null;
  }

  /**
   * Helper class to contain filtering data from Container.Filterable
   */
  class ContainerFilter {

    private final Object propertyId;
    private final String filterString;
    private final boolean onlyMatchPrefix;
    private final boolean ignoreCase;

    public ContainerFilter(Object propertyId, String filterString,
                           boolean ignoreCase, boolean onlyMatchPrefix) {
      this.propertyId = propertyId;
      this.ignoreCase = ignoreCase;
      this.filterString = ignoreCase ? filterString.toLowerCase()
                                     : filterString;
      this.onlyMatchPrefix = onlyMatchPrefix;
    }
  }

  /**
   * Adds container filter for cayenne mapped property. For property not
   * mapped by Cayenne, {@link UnsupportedOperationException} is thrown.
   */
  public void addContainerFilter(Object propertyId, String filterString,
                                 boolean ignoreCase, boolean onlyMatchPrefix) {
    if (addedProperties.containsKey((String)propertyId)) {
      throw new UnsupportedOperationException(
        "CayenneContainer does not support filtering properties not mapped by Cayenne");
    }
    else {
      if (filters == null) {
        filters = new HashSet<ContainerFilter>();
      }
      ContainerFilter f = new ContainerFilter(propertyId, filterString,
                                              ignoreCase, onlyMatchPrefix);
      filters.add(f);
      clearInternalCache();
      fireItemSetChange();
    }
  }

  public void removeAllContainerFilters() {
    if (filters != null) {
      filters = null;
      clearInternalCache();
      fireItemSetChange();
    }
  }

  public void removeContainerFilters(Object propertyId) {
    if (filters != null) {
      for (Iterator<ContainerFilter> iterator = filters.iterator(); iterator
        .hasNext(); ) {
        ContainerFilter f = iterator.next();
        if (f.propertyId.equals(propertyId)) {
          iterator.remove();
        }
      }
      clearInternalCache();
      fireItemSetChange();
    }
  }


  /**
   * CayenneContainer only supports old style addContainerFilter(Object, String,
   * boolean booblean) API and {@link com.vaadin.data.util.filter.SimpleStringFilter}. Support for this
   * newer API maybe in upcoming versions.
   * <p/>
   * Also note that for complex filtering it is possible to override
   * {@link #getBaseExpression()} method and add filter so the query directly.
   *
   * @see #addContainerFilter(Object, String, boolean, boolean)
   * @see com.vaadin.data.Container.Filterable#addContainerFilter(com.vaadin.data.Container.Filter)
   */
  public void addContainerFilter(Filter filter)
    throws UnsupportedFilterException {
    // TODO support new Filter api properly
    if (filter instanceof SimpleStringFilter) {
      SimpleStringFilter sf = (SimpleStringFilter)filter;
      Object propertyId = sf.getPropertyId();
      boolean onlyMatchPrefix = sf.isOnlyMatchPrefix();
      boolean ignoreCase = sf.isIgnoreCase();
      String filterString = sf.getFilterString();
      addContainerFilter(propertyId, filterString, ignoreCase,
                         onlyMatchPrefix);
    }
    else {
      throw new UnsupportedFilterException(
        "CayenneContainer only supports old style addContainerFilter(Object, String, boolean booblean) API. Support for this newer API maybe in upcoming versions.");
    }
  }

  public void removeContainerFilter(Filter filter) {
    // TODO support new Filter api propertly. Even the workaround for
    // SimpleStringFilter works wrong, but hopefully will work good enough
    // for e.g. ComboBox
    if (filter instanceof SimpleStringFilter) {
      SimpleStringFilter sf = (SimpleStringFilter)filter;
      Object propertyId = sf.getPropertyId();
      boolean onlyMatchPrefix = sf.isOnlyMatchPrefix();
      boolean ignoreCase = sf.isIgnoreCase();
      String filterString = sf.getFilterString();
      removeContainerFilters(propertyId);
    }
    // TODO Auto-generated method stub

  }
}
